/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved.
 *
 */

import { findSuitableFilePath, SUPPORT_EXTENSIONS } from '@ohos/hvigor-arkts-compose';
import path from 'path';
import fse from 'fs-extra';

import { ApiVersion } from '../const/sdk-const.js';
import { Dependency, DependencyEnum, DependencyType } from '../project/dependency/core/dependency-interface.js';
import { ModulePkgNode } from '../project/dependency/dependency-manager.js';
import { TargetTaskService } from '../tasks/service/target-task-service.js';

import { OhosLogger } from './log/ohos-logger.js';
import { InjectUtil } from './inject-util.js';
import { CodeType} from "../enum/code-type-enum";
import { BuildArtifactConst, BuildDirConst } from "../const/build-directory-const";
import { DefaultTargetConst } from "../const/common-const";

const logger: OhosLogger = OhosLogger.getLogger('byte-code-har-utils');

const isHsp = (dependency: Dependency) => {
  const dependencyType = dependency.getDependencyType();
  return dependencyType === DependencyType.DEPENDENCY_TYPE_HSP;
};

const isHar = (dependency: Dependency) => {
  const dependencyType = dependency.getDependencyType();
  return dependencyType === DependencyType.DEPENDENCY_TYPE_HAR;
};

const isNpmPackage = (dependency: Dependency) => {
  const dependencyType = dependency.getDependencyType();
  return dependencyType === DependencyType.DEPENDENCY_TYPE_OTHER;
};

const isByteCodeHar = (dependency: Dependency) => {
  return dependency.isByteCodeHarDependency();
};

const isBundledDependency = (dependency: Dependency) => {
  return dependency.isBundleDependency();
};

const isSourceCodeHar = (dependency: Dependency) => {
  return isHar(dependency) && !dependency.isByteCodeHarDependency();
};

const isBlockedByByteCodeHar = (node: ModulePkgNode) => {
  return !!node.parent && isByteCodeHar(node.parent.npm);
};

const getPackageNameFromImportee = (importee: string) => {
  const isStartsWithSymbol = importee.startsWith('@');
  const parts = importee.split(/[/\\]/);
  return isStartsWithSymbol ? parts.slice(0, 2).join('/') : parts[0];
};

export interface IByteCodeHarInfo {
  abcPath: string;
  packageName: string;
  sourceMapsPath?: string;
  insightIntentJsonPath?: string;
  compatibleSdkVersion?: number;
  compatibleSdkVersionStage?: string;
}

interface IParams {
  node: ModulePkgNode,
  queue: ModulePkgNode[];
  extraImportees: Set<string>;
  blockedByByteCodeHar?: boolean;
  byteCodeInfo: Map<string, IByteCodeHarInfo>;
  childrenPendingOrProcessed: Set<string>; // 子依赖收集完毕，或者等待遍历
  otherCompileEntrances: Map<string, boolean>; // Map<入口文件路径, 路径是否为文件夹>
}

/**
 * 过滤掉HSP和devDependencies节点
 * @param nodes
 */
const getFinalNodes = (nodes: ModulePkgNode[]) => {
  const commonNodes = nodes.filter(node => !node.npm.isHspDependency() && !node.npm.isSODependency());
  if (InjectUtil.isByteCodeHarOptimize()) {
    return commonNodes;
  }
  return commonNodes.filter(node => {
    const dependency = node.npm;
    return dependency.getDependencyEnum() !== DependencyEnum.DEV_DEPENDENCIES;
  });
};

/**
 * 收集字节码HAR自身的信息，包括modules.abc和sourceMaps.abc
 * @param dependency
 * @param byteCodeHarInfo
 */
const collectByteCodeHarInfo = (dependency: Dependency, byteCodeHarInfo: Map<string, IByteCodeHarInfo>) => {
  const pkgName = dependency.getPackageName();
  const pkgRoot = dependency.getDependencyRootPath();
  const abcPath = path.join(pkgRoot, CodeType.ETS, BuildArtifactConst.MODULES_ABC);
  // 已经收集过的，不再处理，减少io
  if (byteCodeHarInfo.has(abcPath)) {
    return;
  }
  if (!fse.existsSync(abcPath)) {
    logger.debug(`Dependency [${pkgName}] is byte-code HAR, but '${CodeType.ETS}/${BuildArtifactConst.MODULES_ABC}' does not exists.`);
    return;
  }
  const sourceMapsPath = path.join(pkgRoot, CodeType.ETS, BuildArtifactConst.SOURCEMAPS_MAP);
  const insightIntentJsonPath = path.join(pkgRoot, BuildDirConst.SRC, BuildDirConst.MAIN,
    BuildDirConst.RESOURCES_BASE_PROFILE_DIR, BuildArtifactConst.INSIGHT_INTENT_JSON);
  const pkgJson = dependency.getPackageJsonObj() as any;
  const info: IByteCodeHarInfo = {
    abcPath,
    packageName: pkgName,
    compatibleSdkVersion: pkgJson?.compatibleSdkVersion,
    compatibleSdkVersionStage: pkgJson?.compatibleSdkVersionStage,
    ...(fse.existsSync(sourceMapsPath) ? { sourceMapsPath } : {}),
    ...(fse.existsSync(insightIntentJsonPath) ? { insightIntentJsonPath } : {})
  };
  byteCodeHarInfo.set(pkgRoot, info);
};

/**
 * 收集当前依赖的入口文件，目前适用场景：
 * 1.该依赖为源码HAR，且被字节码HAR直接依赖
 * 2.该依赖为NPM三方包，且被字节码HAR直接依赖
 * @param dependency
 * @param otherCompileEntrances Map<入口文件路径, 路径是否为文件夹>
 */
const collectEntryPath = (dependency: Dependency, otherCompileEntrances: Map<string, boolean>) => {
  const pkgRoot = dependency.getDependencyRootPath();
  const relativePath = dependency.getDependencyMainFileRelativePath();
  const entryPath = relativePath ? path.resolve(pkgRoot, relativePath) : dependency.getDependencyMainFilePath();
  if (!otherCompileEntrances.has(entryPath) && fse.existsSync(entryPath)) {
    otherCompileEntrances.set(entryPath, false);
  }
};

/**
 * 收集当前依赖的src/main/ets文件夹和入口文件，目前适用场景：
 * 1.该依赖为源码HAR，被字节码HAR直接依赖
 * @param dependency
 * @param otherCompileEntrances Map<入口文件路径, 路径是否为文件夹>
 */
const collectSourceCodeHarInfo = (dependency: Dependency, otherCompileEntrances: Map<string, boolean>) => {
  collectEntryPath(dependency, otherCompileEntrances);
  const pkgRoot = dependency.getDependencyRootPath();
  const sourceCodeFolder = path.resolve(pkgRoot, BuildDirConst.SRC, BuildDirConst.MAIN, CodeType.ETS);
  if (!otherCompileEntrances.has(sourceCodeFolder) && fse.existsSync(sourceCodeFolder) && fse.statSync(sourceCodeFolder).isDirectory()) {
    otherCompileEntrances.set(sourceCodeFolder, true);
  }
};

/**
 * 收集当前依赖中的oh-package.json5中的metadata.extraImportees，目前适用场景：
 * 1.该依赖为字节码HAR
 * @param dependency
 * @param extraImportees
 */
const collectExtraImportees = (dependency: Dependency, extraImportees: Set<string>) => {
  const pkgJson = dependency.getPackageJsonObj() as any;
  const importees: string[] = pkgJson?.metadata?.extraImportees || [];
  importees.forEach(importee => {
    extraImportees.add(importee);
  });
};

/**
 * 如果当前依赖为字节码HAR（后续使用H代替），且开启了bundledDependencies特性，则其相关依赖由该函数进行处理，处理逻辑如下：
 * 1.对于已经编译进H的依赖A（目前为编译时依赖链上的源码HAR），直接忽略，依赖A的依赖入队列，直到步骤2
 * 2.对于没有编译进H的依赖B（目前为编译时依赖链上的字节码HAR，NPM三方库，HSP），blockedByByteCodeHar强制为true，走processNode逻辑
 *   为啥要blockedByByteCodeHar强制为true，走processNode逻辑？
 *   试想一下，如果编译H时，其中的某个依赖C为字节码HAR，集成时，依赖C变为源码HAR了（有可能是更新了版本，也有可能是overrides）
 *   那么依赖C，包括其自身的依赖，就要按照现有的收集逻辑进行收集。
 *   同时，依赖C的父级已经被编译进H了，依赖C视为被字节码HAR阻断，也很合理。
 * @param opts
 */
const processBundledNodeDependencies = (opts: IParams) => {
  const { node, childrenPendingOrProcessed } = opts;
  const pkgRoot = node.npm.getDependencyRootPath();
  if (childrenPendingOrProcessed.has(pkgRoot)) {
    return;
  }
  childrenPendingOrProcessed.add(pkgRoot);
  const pkgJson = node.npm.getPackageJsonObj() as any;
  const bundledPkgNames: string[] = pkgJson?.metadata?.compiledPackageNames || [];
  const queue = getFinalNodes(node.children);
  while (queue.length) {
    const cur = queue.shift()!;
    const dependency = cur.npm;
    const pkgName = dependency.getPackageName();
    const alreadyBundled = bundledPkgNames.includes(pkgName);
    // 已经编译进去的，不需要处理了，继续找子节点
    if (alreadyBundled) {
      queue.push(...getFinalNodes(cur.children));
    } else {
      // 没有被编译进去的依赖，需要收集
      processNode({
        ...opts,
        node: cur,
        blockedByByteCodeHar: true, // 显示指定为被字节码HAR阻断
      });
    }
  }
};

/**
 * 如果当前依赖为字节码HAR，且开启了bundledDependencies特性，则由该函数进行处理，处理逻辑如下：
 * 1.收集当前依赖的字节码HAR信息，报错modules.abc和sourceMaps的路径
 * 2.收集当前依赖中的oh-package.json5中的metadata.extraImportees
 * 3.处理其自身的依赖，处理逻辑查看processBundledNodeDependencies
 * @param opts
 */
const processBundledNode = (opts: IParams) => {
  const { node, byteCodeInfo, extraImportees } = opts;
  const dependency = node.npm;
  if (!isBundledDependency(dependency)) {
    return false;
  }
  collectByteCodeHarInfo(dependency, byteCodeInfo);
  collectExtraImportees(dependency, extraImportees);
  processBundledNodeDependencies(opts);
  return true;
};

const enqueueChildren = (opts: IParams) => {
  const { node, childrenPendingOrProcessed, queue } = opts;
  const dependency = node.npm;
  const pkgRoot = dependency.getDependencyRootPath();
  // 已经处理过了的，不需要二次处理
  if (childrenPendingOrProcessed.has(pkgRoot)) {
    return;
  }
  childrenPendingOrProcessed.add(pkgRoot);
  queue.push(...getFinalNodes(node.children));
};

const processByteCodeHarNode = (opts: IParams) => {
  const { node, byteCodeInfo, extraImportees } = opts;
  const dependency = node.npm;
  if (!isByteCodeHar(dependency) || isBundledDependency(dependency)) {
    return false;
  }
  collectByteCodeHarInfo(dependency, byteCodeInfo);
  collectExtraImportees(dependency, extraImportees);
  enqueueChildren(opts);
  return true;
};

const processSourceCodeHarNode = (opts: IParams) => {
  const { node, queue, otherCompileEntrances, blockedByByteCodeHar } = opts;
  const dependency = node.npm;
  if (!isSourceCodeHar(dependency)) {
    return false;
  }
  // 被字节码HAR直接阻断的，需要收集源码文件
  if (blockedByByteCodeHar || isBlockedByByteCodeHar(node)) {
    collectSourceCodeHarInfo(dependency, otherCompileEntrances);
  }
  enqueueChildren(opts);
  return true;
};

const processNpmPackageNode = (opts: IParams) => {
  const { node, queue, otherCompileEntrances, blockedByByteCodeHar } = opts;
  const dependency = node.npm;
  if (!isNpmPackage(dependency)) {
    return false;
  }
  // 被字节码HAR直接阻断的，需要收集入口文件
  if (blockedByByteCodeHar || isBlockedByByteCodeHar(node)) {
    collectEntryPath(dependency, otherCompileEntrances);
  }
  // npm三方包不会存在字节码HAR的依赖，所以其依赖，不再需要遍历
  return true;
};

/**
 * 职责链：
 * 如果某个函数已经处理，则返回true，后续的不需要再处理，顺序可以不固定
 */
const chains = [processBundledNode, processByteCodeHarNode, processSourceCodeHarNode, processNpmPackageNode];

const processNode = (opts: IParams) => {
  chains.some(fn => fn(opts));
};

const processExtraImportees = (targetService: TargetTaskService, extraImportees: Set<string>, otherCompileEntrances: Map<string, boolean>) => {
  const pkgName2Dependency: Map<string, Dependency> = new Map();
  targetService.getModuleService().getOtherDependencies().forEach(dependency => {
    pkgName2Dependency.set(dependency.getPackageName(), dependency);
  });
  extraImportees.forEach(importee => {
    const pkgName = getPackageNameFromImportee(importee);
    if (!pkgName2Dependency.has(pkgName)) {
      logger.debug(`Can not find dependency [${pkgName}], make sure it has been installed successfully`);
      return;
    }
    const dependency = pkgName2Dependency.get(pkgName)!;
    const isEntry = pkgName === importee;
    if (isEntry) {
      collectEntryPath(dependency, otherCompileEntrances);
      return;
    }
    const importeePath = path.join(dependency.getDependencyRootPath(), importee.substring(pkgName.length));
    const filePath = findSuitableFilePath(importeePath, SUPPORT_EXTENSIONS);
    if (!filePath) {
      logger.debug(`Can not find file path by importee: ${importee} [${pkgName}]`);
      return;
    }
    otherCompileEntrances.set(filePath, false);
  });
};

/**
 * 根据targetService收集依赖中字节码HAR的相关信息，目前应用于HAP/HSP/HAR(预览、OhosTest、LocalTest)
 * 收集的数据如下：
 * byteCodeHarInfo: 依赖链上的字节码HAR，包括abc路径，sourceMaps路径
 * otherCompileEntrances: 一些需要额外编译的代码文件，有以下几种：
 *    1.被字节码HAR直接依赖的源码HAR下的ets文件夹路径和入口文件（已经被bundle进字节码HAR的依赖，不需要再写入）
 *    2.被字节码HAR直接依赖的npm三方包的入口文件路径
 *    3.字节码HAR的extraImportees对应的文件路径
 * 已经被bundle进字节码HAR的依赖，这个怎么理解？
 * 字节码HAR有个开关，bundledDependencies，该特性开启后，会把该字节码HAR依赖链上的源码HAR（未被HSP和字节码HAR阻断），都编译到自身的modules.abc中。
 * 举个例子，以编译har为例，同时开启bundleDependencies（->为依赖）：
 * har -> har1(源码) -> har2(源码) -> hsp -> har3(源码)
 * har -> har4(源码) -> har5(字节码) -> har6(源码)
 * har -> har7(源码) -> dayjs(npm)
 * 在编译har时，har1, har2, har4, har7都会编译到自身的modules.abc中，其他则不会。
 * 那么，har的产物在后续被集成到hap/hsp中时，hsp/har3不需要收集，har5需要收集到byteCodeHarInfo中，har6的入口文件和ets文件夹、dayjs的入口文件需要收集到otherCompileEntrances。
 * 假设，在har被集成时，工程级设置overrides，将har5设置为源码HAR，那么此时har5的入口文件和ets文件夹就需要收集到otherCompileEntrances，har6则不再需要收集。
 * 同时，为了提高性能，增加了缓存处理：
 *    1.已经收集过的，或者正在收集的依赖，不再遍历，这块逻辑由childrenPendingOrProcessed承载
 *    2.npm三方包不存在依赖字节码HAR的可能性，其子依赖不需要收集
 * 最后，还有extraImportees的处理，该字段是储存被字节码HAR使用到的NPM三方包的包名+路径引用，形如：dayjs/plugin/etc。
 * 从上文可以看到，被字节码HAR直接依赖的NPM三方包，只会收集入口，如果某个importee（dayjs/plugin/etc）对应的文件没有被入口文件import，就会引起运行时异常，增加该字段，为了补充这部分逻辑的缺失。
 * 在收集时，找到对应的文件，并加入otherCompileEntrances。
 * @param targetService
 */
export const collectByteCodeInfoAndOtherCompileEntrances = (targetService: TargetTaskService) => {
  const service = targetService.getModuleService();
  const directPkgNodes = InjectUtil.isByteCodeHarOptimize() ? service.getAllDirectPkgNodes() : service.getDirectPkgNodes();
  const queue = getFinalNodes(directPkgNodes);
  const extraImportees: Set<string> = new Set();
  const childrenPendingOrProcessed: Set<string> = new Set();
  const otherCompileEntrances: Map<string, boolean> = new Map();
  const byteCodeInfo: Map<string, IByteCodeHarInfo> = new Map();
  while (queue.length) {
    const node = queue.shift()!;
    processNode({
      node,
      queue,
      byteCodeInfo,
      extraImportees,
      childrenPendingOrProcessed,
      otherCompileEntrances
    });
  }
  processExtraImportees(targetService, extraImportees, otherCompileEntrances);
  return {
    byteCodeInfo,
    otherCompileEntrances
  };
};

export const checkByteCodeHar = ({
  logger,
  compileApiVersion,
  compatibleApiVersion,
  useNormalizedOHMUrl,
}: {
  logger: OhosLogger;
  compileApiVersion: number;
  compatibleApiVersion: number;
  useNormalizedOHMUrl?: boolean;
}) => {
  if (compileApiVersion < ApiVersion.API_VERSION_12 || compatibleApiVersion < ApiVersion.API_VERSION_12) {
    logger.printErrorExit('BYTECODEHAR_NOT_SUPPORTED_WHEN_BLOW_API_VERSION_12');
  }
  if (!useNormalizedOHMUrl) {
    logger.printErrorExit('BYTECODEHAR_NOT_SUPPORTED_WHEN_USENORMALLIZEDOHMURL_IS_NOT_TRUE');
  }
};

/**
 * 以下场景下，需要忽略字节码har本身的相关特性，把har当做hap来处理
 * 1. localTest
 * 2. ohosTest
 * 3. 预览
 */
export const shouldTreatHarAsHap = (targetName: string) => {
  return InjectUtil.isLocalTest() || InjectUtil.isPreviewProcess() || (InjectUtil.isOhosTest() && targetName === DefaultTargetConst.OHOS_TEST_TARGET);
};

/**
 * 获取字节码HAR的所有子依赖，不包括HSP
 * 如下结构依赖结构 -> 表示依赖
 * HAP -> byteCodeHar1 -> sourceCodeHar1 -> byteCodeHar2 -> dayjs
 * 返回：
 * {
 *   byteCodeHar1: ['sourceCodeHar1', 'byteCodeHar2', 'dayjs']
 *   byteCodeHar2: ['byteCodeHar2', 'dayjs']
 * }
 * @param targetService
 */
export const realGetByteCodeHarToDependencyKeys = (targetService: TargetTaskService): Map<string, string[]> => {
  const service = targetService.getModuleService();
  const directPkgNodes = InjectUtil.isByteCodeHarOptimize() ? service.getAllDirectPkgNodes() : service.getDirectPkgNodes();
  const nodes = getFinalNodes(directPkgNodes);
  const result: Map<string, string[]> = new Map();
  const cache: Map<string, string[]> = new Map();
  const visitedNodes: Set<string> = new Set();
  const stack = nodes.map(node => ({ node, isVisited: false }));

  // 使用栈实现深度优先遍历
  while (stack.length > 0) {
    const { node, isVisited } = stack.pop()!;
    const dependency = node.npm;
    const dependencyKey = dependency.getDependencyName();
    const children = getFinalNodes(node.children);
    // 首次处理
    if (!isVisited) {
      // 如果没有处理过，需要进栈等待处理
      if (visitedNodes.has(dependencyKey)) {
        logger.debug(`Dependency key: '${dependencyKey}' already processed, just ignore.`);
        continue;
      }
      cache.set(dependencyKey, []);
      visitedNodes.add(dependencyKey);
      // 第一次弹出：标记为已访问，重新压栈，子节点逆序入栈（保证处理顺序）
      stack.push({ node, isVisited: true });
      for (let i = children.length - 1; i >= 0; i--) {
        stack.push({ node: children[i], isVisited: false });
      }
      continue;
    }

    // 第二次弹出：处理当前节点（此时子节点已处理完毕）
    const descendants = cache.get(dependencyKey) || [];
    for (const child of children) {
      const key = child.npm.getDependencyName();
      descendants.push(key);
      if (cache.get(key)?.length) {
        descendants.push(...cache.get(key)!);
      }
    }
    // 只记录字节码HAR
    if (dependency.isByteCodeHarDependency()) {
      result.set(dependencyKey, Array.from(new Set(descendants)));
    }
  }

  logger.debug(`ByteCodeHar2DependencyKeys in module “${service.getModuleModel().getName()}”: ${JSON.stringify(Object.fromEntries(result), null, 2)}`)

  return result;
};

export const getByteCodeHarToDependencyKeys = (targetService: TargetTaskService) => {
  try {
    return realGetByteCodeHarToDependencyKeys(targetService);
  } catch (e) {
    // 由于hvigor无法感知字节码混淆开关是否已经开启，所以报错情况下，输出日志，不阻塞原有流程
    logger.debug(`Execute 'getByteCodeHarToDependencyKeys' failed: ${(e as Error)?.message}`);
    return (new Map() as Map<string, string[]>);
  }
};
